package entity;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

import entity.Iteracao.SituacaoIteracao;
import exception.AlmiranteException;

@Entity
@Table(name="atividade", schema="almirante")
public class Atividade {
	@Id
	@SequenceGenerator(name="atividade_seq", sequenceName="atividade_id_atividade_seq", schema="almirante")
	@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="atividade_seq")
	@Column(name="id_atividade")
	private Long id;
	
	@Column(name="numero", nullable=false)
	private Integer numero;
	
	@Column(name="nome", length=40)
	private String nome;
	
	@Enumerated
	@Column(name="disciplina")
	private Disciplina disciplina;
	
	@Embedded
	@AttributeOverrides({
		@AttributeOverride(name="inicio", column=@Column(name="inicio_estimado")),
		@AttributeOverride(name="fim", column=@Column(name="fim_estimado"))
	})
	private Periodo periodoEstimado;

	@Column(name="horas_estimadas")
	private Integer horasEstimadas = 0;
	
	@ManyToOne
	@JoinColumn(name="id_iteracao", referencedColumnName="id_iteracao")
	private Iteracao iteracao;
	
	
	@ManyToOne
	@JoinColumn(name="id_recurso", referencedColumnName="id_recurso")
	private Recurso responsavel;
	
	@ManyToMany
	@JoinTable(name="atividade_requisito", schema="almirante", 
		joinColumns=@JoinColumn(name="id_atividade"), 
		inverseJoinColumns=@JoinColumn(name="id_requisito"))
	private Set<Requisito> requisitos = new HashSet<Requisito>();
	
	@ManyToMany
	@JoinTable(name="atividade_casouso", schema="almirante", 
		joinColumns=@JoinColumn(name="id_atividade"), 
		inverseJoinColumns=@JoinColumn(name="id_casouso"))
	private Set<CasoUso> casosUso = new HashSet<CasoUso>();
	
	
	@ManyToMany(fetch=FetchType.EAGER, cascade=CascadeType.MERGE)
	@JoinTable(name="atividade_predecessora", schema="almirante", 
		joinColumns=@JoinColumn(name="id_atividade"), 
		inverseJoinColumns=@JoinColumn(name="id_predecessora"))
	private Set<Atividade> atividadesPredecessoras = new HashSet<Atividade>();
	
	@ManyToMany(fetch=FetchType.EAGER, cascade=CascadeType.MERGE)
	@JoinTable(name="atividade_sucessora", schema="almirante", 
		joinColumns=@JoinColumn(name="id_atividade"), 
		inverseJoinColumns=@JoinColumn(name="id_sucessora"))
	private Set<Atividade> atividadesSucessoras = new HashSet<Atividade>();
	
	@OneToMany(mappedBy="atividade", cascade=CascadeType.ALL, orphanRemoval=true)
	@OrderBy("dataCriacao")
	private List<Execucao> execucoes = new ArrayList<Execucao>();
	
	@Enumerated
	@Column(name="status")
	private StatusAtividade status;
	
	Atividade() {}
	
	Atividade(Iteracao iteracao, Date inicio, Date fim) {
		this.periodoEstimado = new Periodo(inicio, fim);
		this.iteracao = iteracao;
		this.status = StatusAtividade.CRIADA;
	}
	
	public Date getInicioEstimado(){
		return periodoEstimado.getInicio();
	}
	
	public Date getFimEstimado() {
		return periodoEstimado.getFim();
	}
	
	public void setPeriodo(Periodo novoPeriodo) {
		Periodo periodoIteracao = new Periodo(iteracao.getInicio(), iteracao.getFim());
		
		if (!periodoIteracao.contem(novoPeriodo))
			throw new AlmiranteException("Datas de início/fim da atividade precisam estar dentro do período da iteração.");
		
		if (!atividadesPredecessoras.isEmpty()) {
			for (Atividade predecessora : atividadesPredecessoras) {
				if (predecessora.getFimEstimado().after(novoPeriodo.getInicio())
						|| predecessora.getFimEstimado().equals(novoPeriodo.getInicio())) {
					throw new AlmiranteException("Data de início da atividade precisa ser posterior ao fim das suas predecessoras.");
				}
			}
		}
		
		// validacoes de fim
		if (!atividadesSucessoras.isEmpty()) {
			// Altera as sucessoras em 2 passos, primeiro valida, depois sai alterando
			for (Atividade sucessora : atividadesSucessoras) {
				if (sucessora.getInicioEstimado().equals(novoPeriodo.getFim()) || sucessora.getInicioEstimado().before(novoPeriodo.getFim())) {
					Periodo novoPeriodoSucessora = novoPeriodo.novoPeriodoPosterior(sucessora.getDuracaoEstimada());
					if (novoPeriodoSucessora.getFim().after(iteracao.getFim()))
						throw new AlmiranteException("Não é possível aumentar a duração da atividade, pois a atividade sucessora #" +
								sucessora.getNumero() + " ultrapassaria o fim da iteração");
				}
			}	
		}
		
		if (!atividadesSucessoras.isEmpty()) {
			for (Atividade sucessora : atividadesSucessoras) {
				if (sucessora.getInicioEstimado().equals(novoPeriodo.getFim()) || sucessora.getInicioEstimado().before(novoPeriodo.getFim())) {
					Periodo novoPeriodoSucessora = novoPeriodo.novoPeriodoPosterior(sucessora.getDuracaoEstimada());
					sucessora.setPeriodo(novoPeriodoSucessora);
				}
			}			
		}
		
		this.periodoEstimado = novoPeriodo;
	}
	
	public Periodo getPeriodo() {
		return periodoEstimado;
	}
	
	public Integer getDuracaoEstimada() {
		return periodoEstimado.intervaloEmDias();
	}
	
	public void setDuracaoEstimada(Integer duracao) {
		Periodo novoPeriodo = periodoEstimado.novoIntervalo(duracao);
		if (novoPeriodo.getFim().after(iteracao.getFim()))
			throw new AlmiranteException("Fim da atividade não pode ultrapassar o fim da iteração");
		
		this.periodoEstimado = novoPeriodo;
	}
	
	public Iteracao getIteracao() {
		return iteracao;
	}

	public String getNome() {
		return nome;
	}

	public void setNome(String nome) {
		this.nome = nome;
	}

	public Integer getNumero() {
		return numero;
	}

	void setNumero(Integer numero) {
		this.numero = numero;
	}

	public Integer getHorasEstimadas() {
		return horasEstimadas;
	}

	public void setHorasEstimadas(Integer horasEstimadas) {
		this.horasEstimadas = horasEstimadas;
	}

	public Long getId() {
		return id;
	}
	
	public Disciplina getDisciplina() {
		return disciplina;
	}

	public void setDisciplina(Disciplina disciplina) {
		this.disciplina = disciplina;
	}

	void setIteracao(Iteracao iteracao) {
		if (iteracao.getInicio().after(getInicioEstimado()))
			throw new AlmiranteException("Início da atividade não pode ser anterior ao início da iteração.");
		if (iteracao.getFim().before(getFimEstimado()))
			throw new AlmiranteException("Fim da atividade não pode ultrapassar o fim da iteração");
		
		this.iteracao = iteracao;
	}
	
	public void setResponsavel(Recurso recurso) {
		iteracao.verificaSeSituacaoPermiteAlteracao();
		if (!iteracao.possuiRecurso(recurso))
			throw new AlmiranteException("Recurso não está vinculado à Iteração.");
		
		this.responsavel = recurso;
	}
	
	public Recurso getResponsavel() {
		return responsavel;
	}
	
	public void adicionarRequisito(Requisito requisito) {
		iteracao.verificaSeSituacaoPermiteAlteracao();
		
		if (!iteracao.possuiRequisito(requisito))
			throw new AlmiranteException("Requisito não está vinculado à Iteração");
		
		requisitos.add(requisito);
	}
	
	public boolean possuiRequisito(Requisito requisito) {
		return requisitos.contains(requisito);
	}
	
	public Set<Requisito> getRequisitos() {
		return Collections.unmodifiableSet(requisitos);
	}
	
	public void removerRequisito(Requisito requisito) {
		iteracao.verificaSeSituacaoPermiteAlteracao();
		requisitos.remove(requisito);
	}
	
	public void adicionarPredecessora(Atividade predecessora) {
		iteracao.verificaSeSituacaoPermiteAlteracao();
		
		if (!predecessora.getIteracao().equals(iteracao))
			throw new AlmiranteException("Predecessora precisa pertencer à mesma iteração!");
		
		if (predecessora.getNumero().equals(this.numero))
			throw new AlmiranteException("Atividade não pode ser predecessora dela mesma!");
		
		if (predecessora.ehSucessoraA(this))
			throw new AlmiranteException("Atividade #" + predecessora.getNumero() + 
					" não pode ser predecessora de #" + numero);
		
		if (predecessora.getFimEstimado().after(getInicioEstimado())
				|| predecessora.getFimEstimado().equals(getInicioEstimado())) {
			try {
				setPeriodo(predecessora.getPeriodo().novoPeriodoPosterior(periodoEstimado.intervaloEmDias()));				
			} catch (AlmiranteException e) {
				throw new AlmiranteException("Não é possível adicionar a atividade #" + predecessora.getNumero() + " como predecessora: " +
						e.getMessage());
			}
		}
		
		atividadesPredecessoras.add(predecessora);
		predecessora.adicionarSucessora(this);
	}
	
	public boolean ehSucessoraA(Atividade atividade) {
		return atividadesPredecessoras.contains(atividade);
	}
	
	public Set<Atividade> getAtividadesPredecessoras() {
		return Collections.unmodifiableSet(atividadesPredecessoras);
	}
	
	public void removerPredecessora(Atividade predecessora) {
		iteracao.verificaSeSituacaoPermiteAlteracao();
		atividadesPredecessoras.remove(predecessora);
		predecessora.removerSucessora(this);
	}
	
	private void adicionarSucessora(Atividade sucessora) {
		atividadesSucessoras.add(sucessora);
	}
	
	private void removerSucessora(Atividade sucessora) {
		atividadesSucessoras.remove(sucessora);
	}
	
	public Set<Atividade> getAtividadesSucessoras() {
		return Collections.unmodifiableSet(atividadesSucessoras);
	}
	
	public void removerTodasPredecessoras() {
		if (!atividadesPredecessoras.isEmpty()) {
			for (Atividade predecessora : atividadesPredecessoras) {
				removerPredecessora(predecessora);
			}
		}
		if (!atividadesSucessoras.isEmpty()) {
			for (Atividade sucessora : atividadesSucessoras) {
				sucessora.removerPredecessora(this);
			}
		}
	}
	
	boolean possuiPredecessora() {
		return !atividadesPredecessoras.isEmpty();
	}
	
	public Execucao adicionarExecucao(Periodo periodo, Integer qtdHoras, String descricao) {
		if (!iteracao.getSituacao().equals(SituacaoIteracao.EM_ANDAMENTO))
			throw new AlmiranteException("Não é possível adicionar execuções, pois a iteração " +
					"não se encontra Em Andamento.");
		
		if (!status.equals(StatusAtividade.EXECUCAO))
			throw new AlmiranteException("Não é possível adicioanr execuções, pois a atividade " +
					"não se encontra Em Execução.");
		
		Execucao execucao = new Execucao(periodo, qtdHoras, descricao, this);
		execucoes.add(execucao);
		
		return execucao;
	}
	
	public List<Execucao> getExecucoes() {
		return execucoes;
	}
	
	public Periodo getPeriodoEstimado() {
		return periodoEstimado;
	}

	public void setPeriodoEstimado(Periodo periodoEstimado) {
		this.periodoEstimado = periodoEstimado;
	}

	public StatusAtividade getStatus() {
		return status;
	}
	
	public void setId(Long id) {
		this.id = id;
	}

	public void setRequisitos(Set<Requisito> requisitos) {
		this.requisitos = requisitos;
	}

	public void setAtividadesPredecessoras(Set<Atividade> atividadesPredecessoras) {
		this.atividadesPredecessoras = atividadesPredecessoras;
	}

	public void setAtividadesSucessoras(Set<Atividade> atividadesSucessoras) {
		this.atividadesSucessoras = atividadesSucessoras;
	}
	
	public Integer getHorasRealizadas() {
		Integer horas = 0;
		if (!execucoes.isEmpty()) {
			for (Execucao execucao : execucoes) {
				horas += execucao.getQtdHoras();
			}
		}
		return horas;
	}
	
	public void finalizar() {
		this.status = StatusAtividade.FINALIZADA;
	}
	
	public void iniciar() {
		if (!podeIniciar())
			throw new AlmiranteException("A ativdade não pode ser iniciada, pois " +
					"se encontra " + status.getDescricao());
		this.status = StatusAtividade.EXECUCAO;
	}
	
	public boolean podeIniciar() {
		return status.equals(StatusAtividade.CRIADA);
	}
	
	public Date getInicioRealizado() {
		if (!execucoes.isEmpty()) {
			SortedSet<Execucao> execucoesOrdenadas = new TreeSet<Execucao>(new Comparator<Execucao>() {
				@Override
				public int compare(Execucao e1, Execucao e2) {
					return e1.getInicio().compareTo(e2.getInicio());
				}
			});
			execucoesOrdenadas.addAll(execucoes);
			
			return execucoesOrdenadas.first().getInicio();
		}
		return null;
	}
	
	public Date getFimRealizado() {
		if (!execucoes.isEmpty()) {
			SortedSet<Execucao> execucoesOrdenadas = new TreeSet<Execucao>(new Comparator<Execucao>() {
				@Override
				public int compare(Execucao e1, Execucao e2) {
					return e1.getFim().compareTo(e2.getFim());
				}
			});
			execucoesOrdenadas.addAll(execucoes);
			
			return execucoesOrdenadas.last().getFim();
		}
		return null;
	}
	
	public void cancelar() {
		if (status.equals(StatusAtividade.FINALIZADA))
			throw new AlmiranteException("Atividade não pode ser cancelada, pois já foi finalizada!");
		status = StatusAtividade.CANCELADA;
	}
	
	public void adicionarCasoUso(CasoUso casoUso) {
		iteracao.verificaSeSituacaoPermiteAlteracao();
		
		if (!iteracao.possuiCasoUso(casoUso))
			throw new AlmiranteException("Caso de Uso " + casoUso.getNomeCasoUso() +
					" não pertence à iteração.");
		
		casosUso.add(casoUso);
		if (!casoUso.getListRequisito().isEmpty()) {
			for (Requisito r : casoUso.getListRequisito()) {
				if (!possuiRequisito(r)) {
					requisitos.add(r);
				}
			}
		}
	}
	
	public boolean possuiCasoUso(CasoUso casoUso) {
		return casosUso.contains(casoUso);
	}
	
	public void removerCasoUso(CasoUso casoUso) {
		iteracao.verificaSeSituacaoPermiteAlteracao();
		casosUso.remove(casoUso);
	}
	
	// equals & hashCode
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Atividade other = (Atividade) obj;
		if (id == null) {
			if (other.id != null)
				return false;
		} else if (!id.equals(other.id))
			return false;
		return true;
	}

}
